#include "CrashReportApp.h"

#include <sstream>
#include <memory>

#include <wx/cmdline.h>
#include <wx/chartype.h>
#include <wx/artprov.h>
#include <wx/filename.h>
#include <wx/stdpaths.h>
#include <wx/wrapsizer.h>
#include <wx/hyperlink.h>

#include "google_breakpad/processor/basic_source_line_resolver.h"
#include "google_breakpad/processor/minidump_processor.h"
#include "google_breakpad/processor/process_state.h"
#include "google_breakpad/processor/minidump.h"
#include "processor/stackwalk_common.h"

#include "warning.xpm"

#include <Internat.h>

#if defined(_WIN32)
#include <locale>
#include <codecvt>
#include "client/windows/sender/crash_report_sender.h"

namespace {
std::wstring ToPlatformString(const std::string& utf8)
{
    return std::wstring_convert<std::codecvt_utf8<std::wstring::traits_type::char_type>, std::wstring::traits_type::char_type>().from_bytes(
        utf8);
}

bool SendMinidump(const std::string& url, const wxString& minidumpPath, const std::map<std::string, std::string>& arguments,
                  const wxString& commentsFilePath)
{
    std::map<std::wstring, std::wstring> files;
    files[L"upload_file_minidump"] = minidumpPath.wc_str();
    if (!commentsFilePath.empty()) {
        files[wxFileName(commentsFilePath).GetFullName().wc_str()] = commentsFilePath.wc_str();
    }

    std::map<std::wstring, std::wstring> parameters;
    for (auto& p : arguments) {
        parameters[ToPlatformString(p.first)] = ToPlatformString(p.second);
    }

    google_breakpad::CrashReportSender sender(L"");

    auto result = sender.SendCrashReport(
        ToPlatformString(url),
        parameters,
        files,
        nullptr
        );
    return result == google_breakpad::RESULT_SUCCEEDED;
}
}
#else

#include "common/linux/http_upload.h"

namespace {
bool SendMinidump(const std::string& url, const wxString& minidumpPath, const std::map<std::string, std::string>& arguments,
                  const wxString& commentsFilePath)
{
    std::map<std::string, std::string> files;
    files["upload_file_minidump"] = minidumpPath.ToStdString();
    if (!commentsFilePath.empty()) {
        files["comments.txt"] = commentsFilePath.ToStdString();
    }

    std::string response, error;
    bool success = google_breakpad::HTTPUpload::SendRequest(
        url,
        arguments,
        files,
        std::string(),
        std::string(),
        std::string(),
        &response,
        NULL,
        &error);

    return success;
}
}

#endif

constexpr bool CrashReportAppHasUserComment = false;

IMPLEMENT_APP(CrashReportApp);
namespace {
std::map<std::string, std::string> parseArguments(const std::string& str)
{
    int TOKEN_IDENTIFIER{ 0 };
    constexpr int TOKEN_EQ{ 1 };
    constexpr int TOKEN_COMMA{ 2 };
    constexpr int TOKEN_VALUE{ 3 };

    int i = 0;

    std::string key;
    int state = TOKEN_COMMA;
    std::map<std::string, std::string> result;
    while (true)
    {
        if (str[i] == 0) {
            break;
        } else if (isspace(str[i])) {
            ++i;
        } else if (isalpha(str[i])) {
            if (state != TOKEN_COMMA) {
                throw std::logic_error("malformed parameters string: unexpected identifier");
            }

            int begin = i;
            while (isalnum(str[i]) || str[i] == '[' || str[i] == ']') {
                ++i;
            }

            key = str.substr(begin, i - begin);
            state = TOKEN_IDENTIFIER;
        } else if (str[i] == '=') {
            if (state != TOKEN_IDENTIFIER) {
                throw std::logic_error("malformed parameters string: unexpected '=' symbol");
            }
            ++i;
            state = TOKEN_EQ;
        } else if (str[i] == '\"') {
            if (state != TOKEN_EQ) {
                throw std::logic_error("malformed parameters string: unexpected '\"' symbol");
            }

            int begin = ++i;
            while (true)
            {
                if (str[i] == 0) {
                    throw std::logic_error("unterminated string literal");
                } else if (str[i] == '\"') {
                    if (i > begin) {
                        result[key] = str.substr(begin, i - begin);
                    } else {
                        result[key] = std::string();
                    }
                    ++i;
                    state = TOKEN_VALUE;
                    break;
                }
                ++i;
            }
        } else if (str[i] == ',') {
            if (state != TOKEN_VALUE) {
                throw std::logic_error("malformed parameters string: unexpected ',' symbol");
            }
            state = TOKEN_COMMA;
            ++i;
        } else {
            throw std::logic_error("malformed parameters string");
        }
    }
    if (state != TOKEN_VALUE) {
        throw std::logic_error("malformed parameters string");
    }

    return result;
}

void PrintMinidump(google_breakpad::Minidump& minidump)
{
    google_breakpad::BasicSourceLineResolver resolver;
    google_breakpad::MinidumpProcessor minidumpProcessor(nullptr, &resolver);
    google_breakpad::MinidumpThreadList::set_max_threads(std::numeric_limits<uint32_t>::max());
    google_breakpad::MinidumpMemoryList::set_max_regions(std::numeric_limits<uint32_t>::max());

    google_breakpad::ProcessState processState;

    if (minidumpProcessor.Process(&minidump, &processState) != google_breakpad::PROCESS_OK) {
        printf("Failed to process minidump");
    } else {
        google_breakpad::PrintProcessState(processState, true, false, &resolver);
    }
}

wxString MakeDumpString(google_breakpad::Minidump& minidump, const wxString& temp)
{
#if _WIN32
    auto stream = _wfreopen(temp.wc_str(), L"w+", stdout);
#else
    auto stream = freopen(temp.utf8_str().data(), "w+", stdout);
#endif
    if (stream == NULL) {
        throw std::runtime_error("Failed to print minidump: cannot open temp file");
    }
    PrintMinidump(minidump);
    fflush(stdout);

    auto length = ftell(stream);
    std::vector<char> bytes(length);
    fseek(stream, 0, SEEK_SET);
    fread(&bytes[0], 1, length, stream);
    fclose(stream);

#if _WIN32
    _wremove(temp.wc_str());
#else
    remove(temp.utf8_str().data());
#endif

    return wxString::From8BitData(&bytes[0], bytes.size());
}

wxString MakeHeaderString(google_breakpad::Minidump& minidump)
{
    if (auto exception = minidump.GetException()) {
        if (auto rawException = exception->exception()) {
            // i18n-hint C++ programming assertion
            return wxString::Format(_("Exception code 0x%x"), rawException->exception_record.exception_code);
        } else {
            // i18n-hint C++ programming assertion
            return _("Unknown exception");
        }
    } else if (auto assertion = minidump.GetAssertion()) {
        auto expression = assertion->expression();
        if (!expression.empty()) {
            return expression;
        }
    }
    return _("Unknown error");
}

void DoShowCrashReportFrame(const wxString& header, const wxString& dump, const std::function<bool(const wxString& comment)>& onSend)
{
    static constexpr int MaxUserCommentLength = 2000;

    auto dialog = new wxDialog(
        nullptr,
        wxID_ANY,
        _("Problem Report for Audacity"),
        wxDefaultPosition,
        wxDefaultSize,
        wxDEFAULT_FRAME_STYLE & ~(wxRESIZE_BORDER | wxMAXIMIZE_BOX)    //disable frame resize
        );

    //fixes focus issue with Windows build-in screen reader, but breaks VoiceOver
#if defined(__WXMSW__)
    dialog->SetFocus();
#endif

    auto mainLayout = new wxBoxSizer(wxVERTICAL);

    auto headerLayout = new wxBoxSizer(wxHORIZONTAL);
    headerLayout->Add(new wxStaticBitmap(dialog, wxID_ANY, wxIcon(warning)));
    headerLayout->AddSpacer(5);

    auto headerText = new wxStaticText(dialog, wxID_ANY, header);
    headerText->SetFont(wxFont(wxFontInfo().Bold()));
    headerLayout->Add(headerText, wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL));

    mainLayout->Add(headerLayout, wxSizerFlags().Border(wxALL));
    if (onSend != nullptr) {
        mainLayout->AddSpacer(5);
        mainLayout->Add(new wxStaticText(dialog, wxID_ANY,
                                         _("Click \"Send\" to submit the report to Audacity. This information is collected anonymously.")),
                        wxSizerFlags().Border(wxALL));
    }
    mainLayout->AddSpacer(10);
    mainLayout->Add(new wxStaticText(dialog, wxID_ANY, _("Problem details")), wxSizerFlags().Border(wxALL));

    auto dumpTextCtrl = new wxTextCtrl(dialog, wxID_ANY, dump, wxDefaultPosition, wxSize(500,
                                                                                         300),
                                       wxTE_RICH | wxTE_READONLY | wxTE_MULTILINE | wxTE_DONTWRAP);
    dumpTextCtrl->SetFont(wxFont(wxFontInfo().Family(wxFONTFAMILY_TELETYPE)));
    dumpTextCtrl->ShowPosition(0);    //scroll to top
    mainLayout->Add(dumpTextCtrl, wxSizerFlags().Border(wxALL).Expand());

    auto buttonsLayout = new wxBoxSizer(wxHORIZONTAL);

    wxTextCtrl* commentCtrl = nullptr;

    if (onSend != nullptr && CrashReportAppHasUserComment) {
        mainLayout->AddSpacer(10);
        mainLayout->Add(new wxStaticText(dialog, wxID_ANY, _("Comments")), wxSizerFlags().Border(wxALL));

        commentCtrl = new wxTextCtrl(dialog, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(500, 100), wxTE_MULTILINE);
        commentCtrl->SetMaxLength(MaxUserCommentLength);

        mainLayout->Add(commentCtrl, wxSizerFlags().Border(wxALL).Expand());
    }

    if (onSend != nullptr) {
        /* i18n-hint: %s will be replaced with "our Privacy Policy" */
        const wxString translatedText = _("See %s for more info.");

        /* i18n-hint: Title of hyperlink to the privacy policy. This is an
           object of "See". */
        const wxString translatedLink = _("our Privacy Policy");

        const size_t placeholderPosition = translatedText.Find(wxT("%s"));

        if (placeholderPosition != wxString::npos) {
            auto privacyPolicyLayout = new wxWrapSizer();

            privacyPolicyLayout->Add(
                new wxStaticText(dialog, wxID_ANY, translatedText.substr(0, placeholderPosition)),
                wxSizerFlags().Proportion(0).Border(wxUP | wxDOWN));

            privacyPolicyLayout->Add(
                new wxHyperlinkCtrl(
                    dialog, wxID_ANY, translatedLink,
                    "https://www.audacityteam.org/about/desktop-privacy-notice/"),
                wxSizerFlags().Proportion(0).Border(wxUP | wxDOWN));

            if (placeholderPosition + 2 < translatedText.Length()) {
                privacyPolicyLayout->Add(
                    new wxStaticText(
                        dialog, wxID_ANY,
                        translatedText.substr(placeholderPosition + 2)),
                    wxSizerFlags().Proportion(1).Border(wxUP | wxDOWN));
            }

            mainLayout->Add(
                privacyPolicyLayout, wxSizerFlags().Border(wxALL));
        }

        auto dontSendButton = new wxButton(dialog, wxID_ANY, XC("&Don't send", "crash reporter button").Translation());
        auto sendButton = new wxButton(dialog, wxID_ANY, XC("&Send", "crash reporter button").Translation());

        dontSendButton->Bind(wxEVT_BUTTON, [dialog](wxCommandEvent&)
        {
            dialog->Close(true);
        });
        sendButton->Bind(wxEVT_BUTTON, [dialog, commentCtrl, onSend](wxCommandEvent&)
        {
            const wxString comment
                =commentCtrl != nullptr
                  ? commentCtrl->GetValue()
                  : wxString {};

            if (onSend(comment)) {
                dialog->Close(true);
            }
        });

        buttonsLayout->Add(dontSendButton);
        buttonsLayout->AddSpacer(5);
        buttonsLayout->Add(sendButton);
    } else {
        auto okButton = new wxButton(dialog, wxID_OK, wxT("OK"));
        okButton->Bind(wxEVT_BUTTON, [dialog](wxCommandEvent&)
        {
            dialog->Close(true);
        });
        buttonsLayout->Add(okButton);
    }

    mainLayout->Add(buttonsLayout, wxSizerFlags().Border(wxALL).Align(wxALIGN_RIGHT));
    dialog->SetSizerAndFit(mainLayout);

    dialog->Bind(wxEVT_CLOSE_WINDOW, [dialog](wxCloseEvent&) {
        dialog->Destroy();
    });

    dialog->Show(true);
}
}

bool CrashReportApp::OnInit()
{
    if (!wxApp::OnInit()) {
        return false;
    }

    if (mSilent) {
        if (!mURL.empty()) {
            SendMinidump(mURL, mMinidumpPath, mArguments, wxEmptyString);
        }
    } else {
        static std::unique_ptr<wxLocale> sLocale(new wxLocale(wxLANGUAGE_DEFAULT));
#if defined(__WXOSX__)
        sLocale->AddCatalogLookupPathPrefix(wxT("../Resources"));
#elif defined(__WXMSW__)
        sLocale->AddCatalogLookupPathPrefix(wxT("Languages"));
#elif defined(__WXGTK__)
        sLocale->AddCatalogLookupPathPrefix(wxT("./locale"));
        sLocale->AddCatalogLookupPathPrefix(wxString::Format(wxT("%s/share/locale"), wxT(INSTALL_PREFIX)));
#endif
        sLocale->AddCatalog("audacity");
        sLocale->AddCatalog("wxstd");

        google_breakpad::Minidump minidump(mMinidumpPath.ToStdString(), false);
        if (minidump.Read()) {
            SetExitOnFrameDelete(true);

            wxFileName temp(mMinidumpPath);
            temp.SetExt("tmp");

            try
            {
                ShowCrashReport(MakeHeaderString(minidump), MakeDumpString(minidump, temp.GetFullPath()));
            }
            catch (std::exception& e)
            {
                wxMessageBox(e.what());
                return false;
            }
            return true;
        }
    }
    return false;
}

void CrashReportApp::OnInitCmdLine(wxCmdLineParser& parser)
{
    static const wxCmdLineEntryDesc cmdLineEntryDesc[] =
    {
        { wxCMD_LINE_SWITCH, "s", "silent", "Send without displaying the confirmation dialog" },
        { wxCMD_LINE_OPTION, "u", "url", "Crash report server URL", wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL },
        { wxCMD_LINE_OPTION, "a", "args", "A set of arguments to send", wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL },
        { wxCMD_LINE_PARAM,  NULL, NULL, "path to minidump file", wxCMD_LINE_VAL_STRING, wxCMD_LINE_OPTION_MANDATORY },
        { wxCMD_LINE_NONE }
    };

    parser.SetDesc(cmdLineEntryDesc);

    wxApp::OnInitCmdLine(parser);
}

bool CrashReportApp::OnCmdLineParsed(wxCmdLineParser& parser)
{
    wxString url;
    wxString arguments;
    if (parser.Found("u", &url)) {
        mURL = url.ToStdString();
    }
    if (parser.Found("a", &arguments)) {
        try
        {
            mArguments = parseArguments(arguments.ToStdString());
        }
        catch (std::exception& e)
        {
            wxMessageBox(e.what());
            return false;
        }
    }
    mMinidumpPath = parser.GetParam(0);
    mSilent = parser.Found("s");

    return wxApp::OnCmdLineParsed(parser);
}

void CrashReportApp::ShowCrashReport(const wxString& header, const wxString& text)
{
    if (mURL.empty()) {
        DoShowCrashReportFrame(header, text, nullptr);
    } else {
        DoShowCrashReportFrame(header, text, [this](const wxString& comments)
        {
            wxString commentsFilePath;
            if (!comments.empty()) {
                wxFileName temp(mMinidumpPath);
                temp.SetName(temp.GetName() + "-comments");
                temp.SetExt("txt");
                commentsFilePath = temp.GetFullPath();
                wxFile file;
                if (file.Open(commentsFilePath, wxFile::write)) {
                    file.Write(comments);
                    file.Close();
                }
            }

            auto result = SendMinidump(mURL, mMinidumpPath, mArguments, commentsFilePath);
            if (!commentsFilePath.empty()) {
                wxRemoveFile(commentsFilePath);
            }

            if (!result) {
                wxMessageBox(_("Failed to send crash report"));
            }
            return result;
        });
    }
}
